import requests
from bs4 import BeautifulSoup
import html2text
import re
import os
from urllib.parse import urljoin
import hashlib
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
from threading import Thread
import time


class CSDNDownloaderApp:
    def __init__(self, root):
        self.root = root
        root.title("CSDN文章下载器 v1.2")
        root.geometry("800x600")

        # 样式配置
        self.style = ttk.Style()
        self.style.configure("TLabel", padding=6, font=('微软雅黑', 10))
        self.style.configure("TButton", padding=6, font=('微软雅黑', 10))
        self.style.configure("TEntry", padding=6, font=('微软雅黑', 10))

        # 创建界面组件
        self.create_widgets()

        # 初始化配置
        self.running = False
        self.download_count = 0
        self.image_count = 0

    def create_widgets(self):
        main_frame = ttk.Frame(self.root, padding=10)
        main_frame.pack(fill=tk.BOTH, expand=True)

        # URL输入部分
        url_frame = ttk.Frame(main_frame)
        url_frame.pack(fill=tk.X, pady=5)

        ttk.Label(url_frame, text="文章URL:").pack(side=tk.LEFT)
        self.url_entry = ttk.Entry(url_frame, width=50)
        self.url_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)

        # 配置部分
        config_frame = ttk.Frame(main_frame)
        config_frame.pack(fill=tk.X, pady=5)

        ttk.Button(config_frame, text="选择输出目录", command=self.choose_output_dir).pack(side=tk.LEFT)
        self.output_dir_var = tk.StringVar(value=os.path.expanduser("~/Desktop/CSDN_Articles"))
        ttk.Label(config_frame, textvariable=self.output_dir_var).pack(side=tk.LEFT, padx=5)

        # 控制按钮
        btn_frame = ttk.Frame(main_frame)
        btn_frame.pack(pady=10)

        self.start_btn = ttk.Button(btn_frame, text="开始下载", command=self.toggle_download)
        self.start_btn.pack(side=tk.LEFT, padx=5)
        ttk.Button(btn_frame, text="清空日志", command=self.clear_log).pack(side=tk.LEFT)

        # 进度条
        self.progress = ttk.Progressbar(main_frame, orient=tk.HORIZONTAL, mode='determinate')
        self.progress.pack(fill=tk.X, pady=5)

        # 日志显示
        log_frame = ttk.Frame(main_frame)
        log_frame.pack(fill=tk.BOTH, expand=True)

        self.log_area = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, font=('Consolas', 9))
        self.log_area.pack(fill=tk.BOTH, expand=True)

    def toggle_download(self):
        if self.running:
            self.running = False
            self.start_btn.config(text="开始下载")
            self.log("操作已中止")
        else:
            self.running = True
            self.start_btn.config(text="停止下载")
            Thread(target=self.start_download).start()

    def choose_output_dir(self):
        directory = filedialog.askdirectory()
        if directory:
            self.output_dir_var.set(directory)

    def clear_log(self):
        self.log_area.delete(1.0, tk.END)

    def log(self, message, status="info"):
        timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
        tag = "INFO" if status == "info" else "ERROR"
        self.log_area.insert(tk.END, f"[{timestamp}] [{tag}] {message}\n")
        self.log_area.see(tk.END)

    def start_download(self):
        url = self.url_entry.get()
        output_dir = self.output_dir_var.get()
        image_dir = "images"

        if not url.startswith(('http://', 'https://')):
            self.log("请输入有效的URL地址", "error")
            return

        try:
            self.download_count = 0
            self.image_count = 0
            self.progress['value'] = 0

            csdn_downloader = CSDNArticleDownloader(
                url=url,
                output_dir=output_dir,
                image_dir=image_dir,
                update_callback=self.update_ui
            )

            success = csdn_downloader.download()
            if success:
                self.log(f"文章下载完成，共下载{self.image_count}张图片")
                messagebox.showinfo("完成", "文章下载完成！")
            else:
                messagebox.showerror("错误", "文章下载失败，请查看日志")

        except Exception as e:
            self.log(f"发生未预期的错误: {str(e)}", "error")
        finally:
            self.running = False
            self.start_btn.config(text="开始下载")
            self.progress['value'] = 0

    def update_ui(self, progress_type, **kwargs):
        if progress_type == "total":
            self.progress['maximum'] = kwargs.get('total', 100)
        elif progress_type == "update":
            self.progress['value'] = kwargs.get('value', 0)
        elif progress_type == "image":
            self.image_count += 1
            self.log(f"已下载图片: {kwargs.get('filename')}")
        elif progress_type == "article":
            self.download_count += 1
            self.log(f"已保存文章: {kwargs.get('filename')}")


class CSDNArticleDownloader:
    def __init__(self, url, output_dir, image_dir, update_callback=None):
        self.url = url
        self.output_dir = output_dir
        self.image_dir = image_dir
        self.update_callback = update_callback
        self.headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
            "Referer": "https://blog.csdn.net/"
        }

    def download(self):
        try:
            # 创建输出目录
            img_dir = os.path.join(self.output_dir, self.image_dir)
            os.makedirs(img_dir, exist_ok=True)

            # 获取网页内容
            response = requests.get(self.url, headers=self.headers, timeout=10)
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')

            # 提取文章信息
            title = self.get_clean_title(soup)
            if not title:
                raise ValueError("无法获取文章标题")

            # 处理文章内容
            article_content = self.process_content(soup)
            if not article_content:
                raise ValueError("无法获取文章内容")

            # 保存Markdown文件
            self.save_markdown(title, article_content)
            return True

        except Exception as e:
            if self.update_callback:
                self.update_callback("error", message=str(e))
            return False

    def get_clean_title(self, soup):
        title_element = soup.find('h1', class_='title-article')
        if not title_element:
            title_element = soup.find('h1', {'id': 'articleContentId'})
        return re.sub(r'[\\/*?:"<>|]', '', title_element.text.strip()) if title_element else "未命名文章"

    def process_content(self, soup):
        article_content = soup.find('div', class_='article_content') or soup.find('article')
        if not article_content:
            return None

        # 处理代码块
        self.process_code_blocks(article_content)

        # 处理图片
        img_map = self.download_images(article_content)

        # 转换为Markdown
        return self.convert_to_markdown(article_content, img_map)

    def process_code_blocks(self, content):
        for code_block in content.find_all('div', class_=re.compile('hljs-main|code-block')):
            code = code_block.find('code')
            if code:
                code_text = code.text
                code_block.decompose()
                new_tag = soup.new_tag('pre')
                new_tag.string = f'```\n{code_text}\n```'
                content.append(new_tag)

    def download_images(self, content):
        img_map = {}
        images = content.find_all('img')
        total_images = len(images)

        if self.update_callback:
            self.update_callback("total", total=total_images + 1)  # 图片数 + 文章保存

        for idx, img in enumerate(images):
            if not self.update_callback:
                break

            img_url = img.get('src') or img.get('data-src')
            if not img_url:
                continue

            full_url = urljoin(self.url, img_url)
            try:
                filename = self.download_single_image(full_url)
                img_map[img_url] = os.path.join(self.image_dir, filename)
                img['src'] = img_map[img_url]
                self.update_callback("image", filename=filename)
            except Exception as e:
                img_map[img_url] = full_url

            self.update_callback("update", value=idx + 1)

        return img_map

    def download_single_image(self, url):
        img_dir = os.path.join(self.output_dir, self.image_dir)
        ext = os.path.splitext(url)[1][:5].split('?')[0]
        filename = hashlib.md5(url.encode()).hexdigest() + ext
        save_path = os.path.join(img_dir, filename)

        if os.path.exists(save_path):
            return filename

        response = requests.get(url, headers=self.headers, stream=True, timeout=15)
        response.raise_for_status()

        with open(save_path, 'wb') as f:
            for chunk in response.iter_content(chunk_size=8192):
                f.write(chunk)

        return filename

    def convert_to_markdown(self, content, img_map):
        h = html2text.HTML2Text()
        h.body_width = 0
        h.emphasis_mark = '*'
        h.wrap_links = False

        md_content = h.handle(str(content))
        md_content = re.sub(r'\n{3,}', '\n\n', md_content)

        # 替换图片链接
        for original, new in img_map.items():
            md_content = md_content.replace(original, new)

        return md_content

    def save_markdown(self, title, content):
        filename = os.path.join(self.output_dir, f"{title}.md")
        with open(filename, 'w', encoding='utf-8') as f:
            f.write(f"# {title}\n\n")
            f.write(content)

        if self.update_callback:
            self.update_callback("article", filename=filename)
    

if __name__ == "__main__":
    root = tk.Tk()
    app = CSDNDownloaderApp(root)
    root.mainloop()